#pragma once

#include <memory>
#include <string>
#include <vector>
#include "fs_types.h"

// Note: 
// 1. any type of pointer/reference parameters should start with any prefix of const, OUT or INOUT.
// 2. callee should guarantee that: 
//    a) if the end time of operation A is before the begin time of operation B, A should always get the status before B (exclude B's effect).
//    b) if the begin time of operation A is after the end time of operation B, A should always get the status after B (include B's effect, if B is successful).
//    c) otherwise, i.e. the execution time of A and B is overlapped, A could either include or exclude B's effect (decided by callee).
// 3. callee could return E_RETRY_LATER if the operation will be blocked by another a long time. (For example, appending big data and can't append another concurrently.)
//    And then, caller should retry this operation later. (For example, caller can enqueue this operation again, and invoke other operations firstly.)

template<typename FSOperator, typename TP>
class AsyncFSOperator
{
	TP& _threadPool;
	FSOperator& _fs;

public:
	typedef std::vector<uint8_t> byte_array;

	AsyncFSOperator(FSOperator& fs, TP& threadPool): _threadPool(threadPool), _fs(fs) { }

	/* ====== OUT data is owned by caller ====== */

	/* Read Operations */
	template<typename Callback>
	void GetSubdirInfoAsync(DIR_ID parentID, const std::string& dirName, OUT DIR_INFO* pDirInfo, Callback&& callback) const
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.GetSubdirInfo(parentID, dirName, *pDirInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void GetSubdirInfosAsync(DIR_ID parentID, OUT std::vector<DIR_INFO>* pDirInfos, Callback&& callback) const
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.GetSubdirInfos(parentID, *pDirInfos);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void GetSubfileInfoAsync(DIR_ID parentID, const std::string& fileName, OUT FILE_INFO* pFileInfo, Callback&& callback) const
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.GetSubfileInfo(parentID, fileName, *pFileInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void GetSubfileInfosAsync(DIR_ID parentID, OUT std::vector<FILE_INFO>* pFileInfos, Callback&& callback) const
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.GetSubfileInfos(parentID, *pFileInfos);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void ReadFileAsync(const FILE_INFO& sourceInfo, uint64_t offset, OUT uint8_t* buf, size_t readCount, OUT size_t* pActualReadCount, Callback&& callback) const
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.ReadFile(sourceInfo, offset, buf, readCount, *pActualReadCount);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	/* Write Operations */
	template<typename Callback>
	void CreateNewDirAsync(DIR_ID parentID, const std::string& newDirName, OUT DIR_INFO* pNewDirInfo, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.CreateNewDir(parentID, newDirName, *pNewDirInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void CreateLinkDirAsync(DIR_ID parentID, const std::string& newDirName, DIR_ID sourceID, OUT DIR_INFO* pNewDirInfo, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.CreateLinkDir(parentID, newDirName, sourceID, *pNewDirInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void RenameDirAsync(DIR_ID parentID, const std::string& orgDirName, const std::string& newDirName, OUT DIR_INFO* pNewDirInfo, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.RenameDir(parentID, orgDirName, newDirName, *pNewDirInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void DeleteDirAsync(DIR_ID parentID, const std::string& orgDirName, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.DeleteDir(parentID, orgDirName);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

private:
	// NOTE: should never change content of buf after invoking this function and before callback
	template<typename Callback>
	void _CreateNewFileAsync(DIR_ID parentID, const std::string& newFileName, const std::shared_ptr<const byte_array>& buf, OUT FILE_INFO* pNewFileInfo, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.CreateNewFile(parentID, newFileName, &buf->front(), buf->size(), *pNewFileInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

public:
	/** NOTE: can change content of buf after invoking this function */
	template<typename Callback>
	void CreateNewFileAsync(DIR_ID parentID, const std::string& newFileName, const uint8_t* buf, size_t bufLen, OUT FILE_INFO* pNewFileInfo, Callback&& callback)
	{
		auto buf_copy = std::make_shared<const byte_array>(buf, buf + bufLen);
		_CreateNewFileAsync(parentID, newFileName, buf_copy, pNewFileInfo, std::forward<Callback>(callback));
	}

	/** NOTE: can change content of buf after invoking this function */
	template<typename Callback>
	void CreateNewFileAsync(DIR_ID parentID, const std::string& newFileName, const byte_array& buf, OUT FILE_INFO* pNewFileInfo, Callback&& callback)
	{
		auto buf_copy = std::make_shared<const byte_array>(buf);
		_CreateNewFileAsync(parentID, newFileName, buf_copy, pNewFileInfo, std::forward<Callback>(callback));
	}

	/** NOTE: if buf is a r-value, i.e. buf will never be used after invoking this function, please use this overload due to its efficiency. */
	template<typename Callback>
	void CreateNewFileAsync(DIR_ID parentID, const std::string& newFileName, byte_array&& buf, OUT FILE_INFO* pNewFileInfo, Callback&& callback)
	{
		auto buf_move = std::make_shared<const byte_array>(std::move(buf));
		_CreateNewFileAsync(parentID, newFileName, buf_move, pNewFileInfo, std::forward<Callback>(callback));
	}

	template<typename Callback>
	void CreateNewFileAsync(DIR_ID parentID, const std::string& newFileName, const std::string& localFileName, OUT FILE_INFO* pNewFileInfo, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.CreateNewFile(parentID, newFileName, localFileName, *pNewFileInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void CreateLinkFileAsync(DIR_ID parentID, const std::string& newFileName, const FILE_INFO& sourceInfo, OUT FILE_INFO* pNewFileInfo, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.CreateLinkFile(parentID, newFileName, sourceInfo, *pNewFileInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void RenameFileAsync(DIR_ID parentID, const std::string& orgFileName, const std::string& newFileName, OUT FILE_INFO* pNewFileInfo, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.RenameFile(parentID, orgFileName, newFileName, *pNewFileInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void DeleteFileAsync(DIR_ID parentID, const std::string& orgFileName, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			const HResult hr = _fs.DeleteFile(parentID, orgFileName);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr);
			return true;
		}); // enqueue a new job
	}

	/* ====== OUT data is owned by callee ====== */

	template<typename Callback>
	void GetSubdirInfoAsync(DIR_ID parentID, const std::string& dirName, Callback&& callback) const
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			DIR_INFO dirInfo;
			const HResult hr = _fs.GetSubdirInfo(parentID, dirName, dirInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(dirInfo));
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void GetSubdirInfosAsync(DIR_ID parentID, Callback&& callback) const
	{
		_threadPool.EnqueueJob([=]()->bool { 
			std::vector<DIR_INFO> dirInfos;
			const HResult hr = _fs.GetSubdirInfos(parentID, dirInfos);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(dirInfos));
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void GetSubfileInfoAsync(DIR_ID parentID, const std::string& fileName, Callback&& callback) const
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			FILE_INFO fileInfo;
			const HResult hr = _fs.GetSubfileInfo(parentID, fileName, fileInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(fileInfo));
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void GetSubfileInfosAsync(DIR_ID parentID, Callback&& callback) const
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			std::vector<FILE_INFO> fileInfos;
			const HResult hr = _fs.GetSubfileInfos(parentID, fileInfos);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(fileInfos));
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void ReadFileAsync(const FILE_INFO& sourceInfo, uint64_t offset, size_t readCount, Callback&& callback) const
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			byte_array buf(readCount);
			size_t actualReadCount;
			const HResult hr = _fs.ReadFile(sourceInfo, offset, &buf.front(), readCount, actualReadCount);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(buf), actualReadCount);
			return true;
		}); // enqueue a new job
	}

	/* Write Operations */
	template<typename Callback>
	void CreateNewDirAsync(DIR_ID parentID, const std::string& newDirName, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			DIR_INFO newDirInfo;
			const HResult hr = _fs.CreateNewDir(parentID, newDirName, newDirInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(newDirInfo));
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void CreateLinkDirAsync(DIR_ID parentID, const std::string& newDirName, DIR_ID sourceID, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			DIR_INFO newDirInfo;
			const HResult hr = _fs.CreateLinkDir(parentID, newDirName, sourceID, newDirInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(newDirInfo));
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void RenameDirAsync(DIR_ID parentID, const std::string& orgDirName, const std::string& newDirName, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			DIR_INFO newDirInfo;
			const HResult hr = _fs.RenameDir(parentID, orgDirName, newDirName, newDirInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(newDirInfo));
			return true;
		}); // enqueue a new job
	}

private:
	/** NOTE: should never change content of buf after invoking this function and before callback */
	template<typename Callback>
	void _CreateNewFileAsync(DIR_ID parentID, const std::string& newFileName, const std::shared_ptr<const byte_array>& buf, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			FILE_INFO newFileInfo;
			const HResult hr = _fs.CreateNewFile(parentID, newFileName, (const int8_t*)&buf->front(), buf->size(), newFileInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(newFileInfo));
			return true;
		}); // enqueue a new job
	}

public:
	/** NOTE: can change content of buf after invoking this function */
	template<typename Callback>
	void CreateNewFileAsync(DIR_ID parentID, const std::string& newFileName, const uint8_t* buf, size_t bufLen, Callback&& callback)
	{
		auto buf_copy = std::make_shared<const byte_array>(buf, buf + bufLen);
		_CreateNewFileAsync(parentID, newFileName, buf_copy, std::forward<Callback>(callback));
	}

	/** NOTE: can change content of buf after invoking this function */
	template<typename Callback>
	void CreateNewFileAsync(DIR_ID parentID, const std::string& newFileName, const byte_array& buf, Callback&& callback)
	{
		auto buf_copy = std::make_shared<const byte_array>(buf);
		_CreateNewFileAsync(parentID, newFileName, buf_copy, std::forward<Callback>(callback));
	}

	/** NOTE: buf is a r-value, i.e. it should not be used after invoking this function */
	template<typename Callback>
	void CreateNewFileAsync(DIR_ID parentID, const std::string& newFileName, byte_array&& buf, Callback&& callback)
	{
		auto buf_move = std::make_shared<const byte_array>(std::move(buf));
		_CreateNewFileAsync(parentID, newFileName, buf_move, std::forward<Callback>(callback));
	}

	template<typename Callback>
	void CreateNewFileAsync(DIR_ID parentID, const std::string& newFileName, const std::string& localFileName, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			FILE_INFO newFileInfo;
			const HResult hr = _fs.CreateNewFile(parentID, newFileName, localFileName, newFileInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(newFileInfo));
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void CreateLinkFileAsync(DIR_ID parentID, const std::string& newFileName, const FILE_INFO& sourceInfo, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			FILE_INFO newFileInfo;
			const HResult hr = _fs.CreateLinkFile(parentID, newFileName, sourceInfo, newFileInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(newFileInfo));
			return true;
		}); // enqueue a new job
	}

	template<typename Callback>
	void RenameFileAsync(DIR_ID parentID, const std::string& orgFileName, const std::string& newFileName, Callback&& callback)
	{
		_threadPool.EnqueueJob([=]()->bool{ 
			FILE_INFO newFileInfo;
			const HResult hr = _fs.RenameFile(parentID, orgFileName, newFileName, newFileInfo);
			if (E_RETRY_LATER == hr)
				return false;
			callback(hr, std::move(newFileInfo));
			return true;
		}); // enqueue a new job
	}
};
 
